﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="EventBinder.cs" company="JDML Consulting Ltd.">
//   Copyright 2011 Photon Project - http://photon.codeplex.com
// </copyright>
// <summary>
//   Defines the EventBinder type.
// </summary>
// --------------------------------------------------------------------------------------------------------------------

namespace Photon
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Linq.Expressions;
    using System.Reflection;
    using Collections;

    public static class EventBinder
    {
        #region Fields

        private static readonly MethodInfo GetMethod = typeof(EventBinder).GetMethods(BindingFlags.Static | BindingFlags.Public)
            .First(x => x.Name == "Get" && x.IsGenericMethodDefinition && x.GetGenericArguments().Length == 1);

        private static readonly MethodInfo CreateBinderMethod = typeof(EventBinder).GetMethods(BindingFlags.Static | BindingFlags.NonPublic)
            .First(x => x.Name == "CreateEventBinder" && x.IsGenericMethodDefinition && x.GetGenericArguments().Length == 2);

        #endregion

        public static IEventBinder Get(Type publisherType, string name)
        {
            #region Validate Arguments

            Guard.ArgumentNull("publisherType", publisherType);

            #endregion

            return (IEventBinder)GetMethod.MakeGenericMethod()
                .Invoke(null, new object[] { name });
        }

        public static IEventBinder Get<TPublisher>(string name) where TPublisher : class
        {
            #region Validate Arguments

            Guard.ArgumentNull("name", name);

            #endregion

            return EventBinderCache<TPublisher>.GetEventBinder(name);
        }

        public static IEventBinder<TPublisher, TDelegate> Get<TPublisher, TDelegate>(string name)
            where TPublisher : class
            where TDelegate : class
        {
            #region Validate Arguments

            Guard.ArgumentNull("name", name);

            #endregion

            return (IEventBinder<TPublisher, TDelegate>)EventBinderCache<TPublisher>.GetEventBinder(name);
        }

        private static IEventBinder CreateEventBinder(EventInfo eventInfo)
        {
            var method = eventInfo.EventHandlerType.GetMethod("Invoke");
            if (method != null)
            {
                var createMethod = CreateBinderMethod.MakeGenericMethod(eventInfo.ReflectedType,
                    eventInfo.EventHandlerType);
                return (IEventBinder)createMethod.Invoke(null, new object[] { eventInfo });
            }

            return null;
        }

        // ReSharper disable UnusedMember.Local

        private static IEventBinder CreateEventBinder<TPublisher, TDelegate>(EventInfo eventInfo) where TDelegate : class
        {
            return new EventBinder<TPublisher, TDelegate>(eventInfo.Name,
                GenerateAddRemoveMethod<TPublisher, TDelegate>(eventInfo.GetAddMethod()),
                GenerateAddRemoveMethod<TPublisher, TDelegate>(eventInfo.GetRemoveMethod()));
        }

        // ReSharper restore UnusedMember.Local

        private static Action<TPublisher, TDelegate> GenerateAddRemoveMethod<TPublisher, TDelegate>(MethodInfo method)
        {
            // define parameter expressions
            var sourceParameter = Expression.Parameter(typeof(TPublisher), "source");
            var delegateParameter = Expression.Parameter(typeof(TDelegate), "delegate");

            // generate add/remove with cast to target delegate type
            return Expression.Lambda<Action<TPublisher, TDelegate>>(
                        Expression.Call(
                            sourceParameter,
                            method,
                            delegateParameter),
                        sourceParameter, delegateParameter).Compile();
        }

        private static class EventBinderCache<TPublisher> where TPublisher : class
        {
            #region Fields

            private static readonly Dictionary<string, IEventBinder> EventBinders = InitializeCache();

            #endregion

            private static Dictionary<string, IEventBinder> InitializeCache()
            {
                var result = new Dictionary<string, IEventBinder>();
                typeof(TPublisher).GetEvents(BindingFlags.Instance | BindingFlags.Public)
                    .ForEach(x => result.Add(x.Name, CreateEventBinder(x)));
                return result;
            }

            public static IEventBinder GetEventBinder(string name)
            {
                IEventBinder result;
                EventBinders.TryGetValue(name, out result);
                return result;
            }
        }
    }

    public class EventBinder<TPublisher, TDelegate> : IEventBinder<TPublisher, TDelegate> where TDelegate : class
    {
        #region Fields

        private readonly string _eventName;
        private readonly Func<TPublisher, TDelegate, object> _add;
        private readonly Action<TPublisher, object, TDelegate> _remove;

        #endregion


        public EventBinder(string eventName, Func<TPublisher, TDelegate, object> add, Action<TPublisher, object, TDelegate> remove)
        {
            #region Validate Arguments

            WeakDelegateRuntime.ThrowIfTypeIsNotADelegate(typeof(TDelegate));
            Guard.ArgumentNullOrEmpty("eventName", eventName);
            Guard.ArgumentNull("add", add);
            Guard.ArgumentNull("remove", remove);

            #endregion

            _eventName = eventName;
            _add = add;
            _remove = remove;
        }

        public EventBinder(string eventName, Action<TPublisher, TDelegate> add, Action<TPublisher, TDelegate> remove)
            : this(eventName, ConvertAdd(add), ConvertRemove(remove))
        {

        }

        private static Action<TPublisher, object, TDelegate> ConvertRemove(Action<TPublisher, TDelegate> remove)
        {
            Guard.ArgumentNull("remove", remove);
            return (publisher, token, handler) => remove(publisher, handler);
        }

        private static Func<TPublisher, TDelegate, object> ConvertAdd(Action<TPublisher, TDelegate> add)
        {
            Guard.ArgumentNull("add", add);
            return (publisher, handler) =>
            {
                add(publisher, handler);
                return null;
            };
        }

        public string EventName
        {
            get { return _eventName; }
        }

        public Type PublisherType
        {
            get { return typeof(TPublisher); }
        }

        public Type EventType
        {
            get { return typeof(TDelegate); }
        }

        public bool Equals(EventBinder<TPublisher, TDelegate> other)
        {
            if (ReferenceEquals(null, other))
            {
                return false;
            }
            if (ReferenceEquals(this, other))
            {
                return true;
            }
            return Equals(other._eventName, _eventName);
        }

        public override bool Equals(object obj)
        {
            return Equals(obj as EventBinder<TPublisher, TDelegate>);
        }

        public override int GetHashCode()
        {
            return PublisherType.GetHashCode() ^ (13 * EventName.GetHashCode());
        }

        object IEventBinder.Add(object publisher, Delegate handler)
        {
            return Add((TPublisher)publisher, (TDelegate)(object)handler);
        }

        void IEventBinder.Remove(object publisher, object subscriptionToken, Delegate handler)
        {
            Remove((TPublisher)publisher, subscriptionToken, (TDelegate)(object)handler);
        }

        public object Add(TPublisher publisher, TDelegate handler)
        {
            return _add(publisher, handler);
        }

        public void Remove(TPublisher publisher, object subscriptionToken, TDelegate handler)
        {
            _remove(publisher, subscriptionToken, handler);
        }
    }
}